/**
 * Copyright (c) 2006-2017, JGraph Ltd
 * Copyright (c) 2006-2017, Gaudenz Alder
 */
/**
 * Class: mxCellRenderer
 *
 * Renders cells into a document object model. The <defaultShapes> is a global
 * map of shapename, constructor pairs that is used in all instances. You can
 * get a list of all available shape names using the following code.
 *
 * In general the cell renderer is in charge of creating, redrawing and
 * destroying the shape and label associated with a cell state, as well as
 * some other graphical objects, namely controls and overlays. The shape
 * hieararchy in the display (ie. the hierarchy in which the DOM nodes
 * appear in the document) does not reflect the cell hierarchy. The shapes
 * are a (flat) sequence of shapes and labels inside the draw pane of the
 * graph view, with some exceptions, namely the HTML labels being placed
 * directly inside the graph container for certain browsers.
 *
 * (code)
 * mxLog.show();
 * for (var i in mxCellRenderer.defaultShapes)
 * {
 *   mxLog.debug(i);
 * }
 * (end)
 *
 * Constructor: mxCellRenderer
 *
 * Constructs a new cell renderer with the following built-in shapes:
 * arrow, rectangle, ellipse, rhombus, image, line, label, cylinder,
 * swimlane, connector, actor and cloud.
 */
function mxCellRenderer() { };

/**
 * Variable: defaultShapes
 *
 * Static array that contains the globally registered shapes which are
 * known to all instances of this class. For adding new shapes you should
 * use the static <mxCellRenderer.registerShape> function.
 */
mxCellRenderer.defaultShapes = new Object();

/**
 * Variable: defaultEdgeShape
 *
 * Defines the default shape for edges. Default is <mxConnector>.
 */
mxCellRenderer.prototype.defaultEdgeShape = mxConnector;

/**
 * Variable: defaultVertexShape
 *
 * Defines the default shape for vertices. Default is <mxRectangleShape>.
 */
mxCellRenderer.prototype.defaultVertexShape = mxRectangleShape;

/**
 * Variable: defaultTextShape
 *
 * Defines the default shape for labels. Default is <mxText>.
 */
mxCellRenderer.prototype.defaultTextShape = mxText;

/**
 * Variable: legacyControlPosition
 *
 * Specifies if the folding icon should ignore the horizontal
 * orientation of a swimlane. Default is true.
 */
mxCellRenderer.prototype.legacyControlPosition = true;

/**
 * Variable: legacySpacing
 *
 * Specifies if spacing and label position should be ignored if overflow is
 * fill or width. Default is true for backwards compatiblity.
 */
mxCellRenderer.prototype.legacySpacing = true;

/**
 * Variable: antiAlias
 *
 * Anti-aliasing option for new shapes. Default is true.
 */
mxCellRenderer.prototype.antiAlias = true;

/**
 * Variable: minSvgStrokeWidth
 *
 * Minimum stroke width for SVG output.
 */
mxCellRenderer.prototype.minSvgStrokeWidth = 1;

/**
 * Variable: forceControlClickHandler
 *
 * Specifies if the enabled state of the graph should be ignored in the control
 * click handler (to allow folding in disabled graphs). Default is false.
 */
mxCellRenderer.prototype.forceControlClickHandler = false;

/**
 * Function: registerShape
 *
 * Registers the given constructor under the specified key in this instance
 * of the renderer.
 *
 * Example:
 *
 * (code)
 * mxCellRenderer.registerShape(mxConstants.SHAPE_RECTANGLE, mxRectangleShape);
 * (end)
 *
 * Parameters:
 *
 * key - String representing the shape name.
 * shape - Constructor of the <mxShape> subclass.
 */
mxCellRenderer.registerShape = function(key, shape)
{
    mxCellRenderer.defaultShapes[key] = shape;
};

// Adds default shapes into the default shapes array
mxCellRenderer.registerShape(mxConstants.SHAPE_RECTANGLE, mxRectangleShape);
mxCellRenderer.registerShape(mxConstants.SHAPE_ELLIPSE, mxEllipse);
mxCellRenderer.registerShape(mxConstants.SHAPE_RHOMBUS, mxRhombus);
mxCellRenderer.registerShape(mxConstants.SHAPE_CYLINDER, mxCylinder);
mxCellRenderer.registerShape(mxConstants.SHAPE_CONNECTOR, mxConnector);
mxCellRenderer.registerShape(mxConstants.SHAPE_ACTOR, mxActor);
mxCellRenderer.registerShape(mxConstants.SHAPE_TRIANGLE, mxTriangle);
mxCellRenderer.registerShape(mxConstants.SHAPE_HEXAGON, mxHexagon);
mxCellRenderer.registerShape(mxConstants.SHAPE_CLOUD, mxCloud);
mxCellRenderer.registerShape(mxConstants.SHAPE_LINE, mxLine);
mxCellRenderer.registerShape(mxConstants.SHAPE_ARROW, mxArrow);
mxCellRenderer.registerShape(mxConstants.SHAPE_ARROW_CONNECTOR, mxArrowConnector);
mxCellRenderer.registerShape(mxConstants.SHAPE_DOUBLE_ELLIPSE, mxDoubleEllipse);
mxCellRenderer.registerShape(mxConstants.SHAPE_SWIMLANE, mxSwimlane);
mxCellRenderer.registerShape(mxConstants.SHAPE_IMAGE, mxImageShape);
mxCellRenderer.registerShape(mxConstants.SHAPE_LABEL, mxLabel);

/**
 * Function: initializeShape
 *
 * Initializes the shape in the given state by calling its init method with
 * the correct container after configuring it using <configureShape>.
 *
 * Parameters:
 *
 * state - <mxCellState> for which the shape should be initialized.
 */
mxCellRenderer.prototype.initializeShape = function(state)
{
    state.shape.dialect = state.view.graph.dialect;
    this.configureShape(state);
    state.shape.init(state.view.getDrawPane());
};

/**
 * Function: createShape
 *
 * Creates and returns the shape for the given cell state.
 *
 * Parameters:
 *
 * state - <mxCellState> for which the shape should be created.
 */
mxCellRenderer.prototype.createShape = function(state)
{
    var shape = null;

    if (state.style != null)
    {
        // Checks if there is a stencil for the name and creates
        // a shape instance for the stencil if one exists
        var stencil = mxStencilRegistry.getStencil(state.style[mxConstants.STYLE_SHAPE]);

        if (stencil != null)
        {
            shape = new mxShape(stencil);
        }
        else
        {
            var ctor = this.getShapeConstructor(state);
            shape = new ctor();
        }
    }

    return shape;
};

/**
 * Function: createIndicatorShape
 *
 * Creates the indicator shape for the given cell state.
 *
 * Parameters:
 *
 * state - <mxCellState> for which the indicator shape should be created.
 */
mxCellRenderer.prototype.createIndicatorShape = function(state)
{
    state.shape.indicatorShape = this.getShape(state.view.graph.getIndicatorShape(state));
};

/**
 * Function: getShape
 *
 * Returns the shape for the given name from <defaultShapes>.
 */
mxCellRenderer.prototype.getShape = function(name)
{
    return (name != null) ? mxCellRenderer.defaultShapes[name] : null;
};

/**
 * Function: getShapeConstructor
 *
 * Returns the constructor to be used for creating the shape.
 */
mxCellRenderer.prototype.getShapeConstructor = function(state)
{
    var ctor = this.getShape(state.style[mxConstants.STYLE_SHAPE]);

    if (ctor == null)
    {
        ctor = (state.view.graph.getModel().isEdge(state.cell)) ?
            this.defaultEdgeShape : this.defaultVertexShape;
    }

    return ctor;
};

/**
 * Function: configureShape
 *
 * Configures the shape for the given cell state.
 *
 * Parameters:
 *
 * state - <mxCellState> for which the shape should be configured.
 */
mxCellRenderer.prototype.configureShape = function(state)
{
    state.shape.apply(state);
    state.shape.image = state.view.graph.getImage(state);
    state.shape.indicatorColor = state.view.graph.getIndicatorColor(state);
    state.shape.indicatorStrokeColor = state.style[mxConstants.STYLE_INDICATOR_STROKECOLOR];
    state.shape.indicatorGradientColor = state.view.graph.getIndicatorGradientColor(state);
    state.shape.indicatorDirection = state.style[mxConstants.STYLE_INDICATOR_DIRECTION];
    state.shape.indicatorImage = state.view.graph.getIndicatorImage(state);

    this.postConfigureShape(state);
};

/**
 * Function: postConfigureShape
 *
 * Replaces any reserved words used for attributes, eg. inherit,
 * indicated or swimlane for colors in the shape for the given state.
 * This implementation resolves these keywords on the fill, stroke
 * and gradient color keys.
 */
mxCellRenderer.prototype.postConfigureShape = function(state)
{
    if (state.shape != null)
    {
        this.resolveColor(state, 'indicatorColor', mxConstants.STYLE_FILLCOLOR);
        this.resolveColor(state, 'indicatorGradientColor', mxConstants.STYLE_GRADIENTCOLOR);
        this.resolveColor(state, 'fill', mxConstants.STYLE_FILLCOLOR);
        this.resolveColor(state, 'stroke', mxConstants.STYLE_STROKECOLOR);
        this.resolveColor(state, 'gradient', mxConstants.STYLE_GRADIENTCOLOR);
    }
};

/**
 * Function: checkPlaceholderStyles
 *
 * Resolves special keywords 'inherit', 'indicated' and 'swimlane' and sets
 * the respective color on the shape.
 */
mxCellRenderer.prototype.checkPlaceholderStyles = function(state)
{
    // LATER: Check if the color has actually changed
    if (state.style != null)
    {
        var values = ['inherit', 'swimlane', 'indicated'];
        var styles = [mxConstants.STYLE_FILLCOLOR, mxConstants.STYLE_STROKECOLOR, mxConstants.STYLE_GRADIENTCOLOR];

        for (var i = 0; i < styles.length; i++)
        {
            if (mxUtils.indexOf(values, state.style[styles[i]]) >= 0)
            {
                return true;
            }
        }
    }

    return false;
};

/**
 * Function: resolveColor
 *
 * Resolves special keywords 'inherit', 'indicated' and 'swimlane' and sets
 * the respective color on the shape.
 */
mxCellRenderer.prototype.resolveColor = function(state, field, key)
{
    var value = state.shape[field];
    var graph = state.view.graph;
    var referenced = null;

    if (value == 'inherit')
    {
        referenced = graph.model.getParent(state.cell);
    }
    else if (value == 'swimlane')
    {
        state.shape[field] = (key == mxConstants.STYLE_STROKECOLOR) ? '#000000' : '#ffffff';

        if (graph.model.getTerminal(state.cell, false) != null)
        {
            referenced = graph.model.getTerminal(state.cell, false);
        }
        else
        {
            referenced = state.cell;
        }

        referenced = graph.getSwimlane(referenced);
        key = graph.swimlaneIndicatorColorAttribute;
    }
    else if (value == 'indicated')
    {
        state.shape[field] = state.shape.indicatorColor;
    }

    if (referenced != null)
    {
        var rstate = graph.getView().getState(referenced);
        state.shape[field] = null;

        if (rstate != null)
        {
            if (rstate.shape != null && field != 'indicatorColor')
            {
                state.shape[field] = rstate.shape[field];
            }
            else
            {
                state.shape[field] = rstate.style[key];
            }
        }
    }
};

/**
 * Function: getLabelValue
 *
 * Returns the value to be used for the label.
 *
 * Parameters:
 *
 * state - <mxCellState> for which the label should be created.
 */
mxCellRenderer.prototype.getLabelValue = function(state)
{
    return state.view.graph.getLabel(state.cell);
};

/**
 * Function: createLabel
 *
 * Creates the label for the given cell state.
 *
 * Parameters:
 *
 * state - <mxCellState> for which the label should be created.
 */
mxCellRenderer.prototype.createLabel = function(state, value)
{
    var graph = state.view.graph;
    var isEdge = graph.getModel().isEdge(state.cell);

    if (state.style[mxConstants.STYLE_FONTSIZE] > 0 || state.style[mxConstants.STYLE_FONTSIZE] == null)
    {
        // Avoids using DOM node for empty labels
        var isForceHtml = (graph.isHtmlLabel(state.cell) || (value != null && mxUtils.isNode(value)));

        state.text = new this.defaultTextShape(value, new mxRectangle(),
            (state.style[mxConstants.STYLE_ALIGN] || mxConstants.ALIGN_CENTER),
            graph.getVerticalAlign(state),
            state.style[mxConstants.STYLE_FONTCOLOR],
            state.style[mxConstants.STYLE_FONTFAMILY],
            state.style[mxConstants.STYLE_FONTSIZE],
            state.style[mxConstants.STYLE_FONTSTYLE],
            state.style[mxConstants.STYLE_SPACING],
            state.style[mxConstants.STYLE_SPACING_TOP],
            state.style[mxConstants.STYLE_SPACING_RIGHT],
            state.style[mxConstants.STYLE_SPACING_BOTTOM],
            state.style[mxConstants.STYLE_SPACING_LEFT],
            state.style[mxConstants.STYLE_HORIZONTAL],
            state.style[mxConstants.STYLE_LABEL_BACKGROUNDCOLOR],
            state.style[mxConstants.STYLE_LABEL_BORDERCOLOR],
            graph.isWrapping(state.cell) && graph.isHtmlLabel(state.cell),
            graph.isLabelClipped(state.cell),
            state.style[mxConstants.STYLE_OVERFLOW],
            state.style[mxConstants.STYLE_LABEL_PADDING],
            mxUtils.getValue(state.style, mxConstants.STYLE_TEXT_DIRECTION, mxConstants.DEFAULT_TEXT_DIRECTION));
        state.text.opacity = mxUtils.getValue(state.style, mxConstants.STYLE_TEXT_OPACITY, 100);
        state.text.dialect = (isForceHtml) ? mxConstants.DIALECT_STRICTHTML : state.view.graph.dialect;
        state.text.style = state.style;
        state.text.state = state;
        this.initializeLabel(state, state.text);

        // Workaround for touch devices routing all events for a mouse gesture
        // (down, move, up) via the initial DOM node. IE additionally redirects
        // the event via the initial DOM node but the event source is the node
        // under the mouse, so we need to check if this is the case and force
        // getCellAt for the subsequent mouseMoves and the final mouseUp.
        var forceGetCell = false;

        var getState = function(evt)
        {
            var result = state;

            if (mxClient.IS_TOUCH || forceGetCell)
            {
                var x = mxEvent.getClientX(evt);
                var y = mxEvent.getClientY(evt);

                // Dispatches the drop event to the graph which
                // consumes and executes the source function
                var pt = mxUtils.convertPoint(graph.container, x, y);
                result = graph.view.getState(graph.getCellAt(pt.x, pt.y));
            }

            return result;
        };

        // TODO: Add handling for special touch device gestures
        mxEvent.addGestureListeners(state.text.node,
            mxUtils.bind(this, function(evt)
            {
                if (this.isLabelEvent(state, evt))
                {
                    graph.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt, state));
                    forceGetCell = graph.dialect != mxConstants.DIALECT_SVG &&
                        mxEvent.getSource(evt).nodeName == 'IMG';
                }
            }),
            mxUtils.bind(this, function(evt)
            {
                if (this.isLabelEvent(state, evt))
                {
                    graph.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt, getState(evt)));
                }
            }),
            mxUtils.bind(this, function(evt)
            {
                if (this.isLabelEvent(state, evt))
                {
                    graph.fireMouseEvent(mxEvent.MOUSE_UP, new mxMouseEvent(evt, getState(evt)));
                    forceGetCell = false;
                }
            })
        );

        // Uses double click timeout in mxGraph for quirks mode
        if (graph.nativeDblClickEnabled)
        {
            mxEvent.addListener(state.text.node, 'dblclick',
                mxUtils.bind(this, function(evt)
                {
                    if (this.isLabelEvent(state, evt))
                    {
                        graph.dblClick(evt, state.cell);
                        mxEvent.consume(evt);
                    }
                })
            );
        }
    }
};

/**
 * Function: initializeLabel
 *
 * Initiailzes the label with a suitable container.
 *
 * Parameters:
 *
 * state - <mxCellState> whose label should be initialized.
 */
mxCellRenderer.prototype.initializeLabel = function(state, shape)
{
    if (mxClient.IS_SVG && mxClient.NO_FO && shape.dialect != mxConstants.DIALECT_SVG)
    {
        shape.init(state.view.graph.container);
    }
    else
    {
        shape.init(state.view.getDrawPane());
    }
};

/**
 * Function: createCellOverlays
 *
 * Creates the actual shape for showing the overlay for the given cell state.
 *
 * Parameters:
 *
 * state - <mxCellState> for which the overlay should be created.
 */
mxCellRenderer.prototype.createCellOverlays = function(state)
{
    var graph = state.view.graph;
    var overlays = graph.getCellOverlays(state.cell);
    var dict = null;

    if (overlays != null)
    {
        dict = new mxDictionary();

        for (var i = 0; i < overlays.length; i++)
        {
            var shape = (state.overlays != null) ? state.overlays.remove(overlays[i]) : null;

            if (shape == null)
            {
                var tmp = new mxImageShape(new mxRectangle(), overlays[i].image.src);
                tmp.dialect = state.view.graph.dialect;
                tmp.preserveImageAspect = false;
                tmp.overlay = overlays[i];
                this.initializeOverlay(state, tmp);
                this.installCellOverlayListeners(state, overlays[i], tmp);

                if (overlays[i].cursor != null)
                {
                    tmp.node.style.cursor = overlays[i].cursor;
                }

                dict.put(overlays[i], tmp);
            }
            else
            {
                dict.put(overlays[i], shape);
            }
        }
    }

    // Removes unused
    if (state.overlays != null)
    {
        state.overlays.visit(function(id, shape)
        {
            shape.destroy();
        });
    }

    state.overlays = dict;
};

/**
 * Function: initializeOverlay
 *
 * Initializes the given overlay.
 *
 * Parameters:
 *
 * state - <mxCellState> for which the overlay should be created.
 * overlay - <mxImageShape> that represents the overlay.
 */
mxCellRenderer.prototype.initializeOverlay = function(state, overlay)
{
    overlay.init(state.view.getOverlayPane());
};

/**
 * Function: installOverlayListeners
 *
 * Installs the listeners for the given <mxCellState>, <mxCellOverlay> and
 * <mxShape> that represents the overlay.
 */
mxCellRenderer.prototype.installCellOverlayListeners = function(state, overlay, shape)
{
    var graph  = state.view.graph;

    mxEvent.addListener(shape.node, 'click', function (evt)
    {
        if (graph.isEditing())
        {
            graph.stopEditing(!graph.isInvokesStopCellEditing());
        }

        overlay.fireEvent(new mxEventObject(mxEvent.CLICK,
            'event', evt, 'cell', state.cell));
    });

    mxEvent.addGestureListeners(shape.node,
        function (evt)
        {
            mxEvent.consume(evt);
        },
        function (evt)
        {
            graph.fireMouseEvent(mxEvent.MOUSE_MOVE,
                new mxMouseEvent(evt, state));
        });

    if (mxClient.IS_TOUCH)
    {
        mxEvent.addListener(shape.node, 'touchend', function (evt)
        {
            overlay.fireEvent(new mxEventObject(mxEvent.CLICK,
                'event', evt, 'cell', state.cell));
        });
    }
};

/**
 * Function: createControl
 *
 * Creates the control for the given cell state.
 *
 * Parameters:
 *
 * state - <mxCellState> for which the control should be created.
 */
mxCellRenderer.prototype.createControl = function(state)
{
    var graph = state.view.graph;
    var image = graph.getFoldingImage(state);

    if (graph.foldingEnabled && image != null)
    {
        if (state.control == null)
        {
            var b = new mxRectangle(0, 0, image.width, image.height);
            state.control = new mxImageShape(b, image.src);
            state.control.preserveImageAspect = false;
            state.control.dialect = graph.dialect;

            this.initControl(state, state.control, true, this.createControlClickHandler(state));
        }
    }
    else if (state.control != null)
    {
        state.control.destroy();
        state.control = null;
    }
};

/**
 * Function: createControlClickHandler
 *
 * Hook for creating the click handler for the folding icon.
 *
 * Parameters:
 *
 * state - <mxCellState> whose control click handler should be returned.
 */
mxCellRenderer.prototype.createControlClickHandler = function(state)
{
    var graph = state.view.graph;

    return mxUtils.bind(this, function (evt)
    {
        if (this.forceControlClickHandler || graph.isEnabled())
        {
            var collapse = !graph.isCellCollapsed(state.cell);
            graph.foldCells(collapse, false, [state.cell], null, evt);
            mxEvent.consume(evt);
        }
    });
};

/**
 * Function: initControl
 *
 * Initializes the given control and returns the corresponding DOM node.
 *
 * Parameters:
 *
 * state - <mxCellState> for which the control should be initialized.
 * control - <mxShape> to be initialized.
 * handleEvents - Boolean indicating if mousedown and mousemove should fire events via the graph.
 * clickHandler - Optional function to implement clicks on the control.
 */
mxCellRenderer.prototype.initControl = function(state, control, handleEvents, clickHandler)
{
    var graph = state.view.graph;

    // In the special case where the label is in HTML and the display is SVG the image
    // should go into the graph container directly in order to be clickable. Otherwise
    // it is obscured by the HTML label that overlaps the cell.
    var isForceHtml = graph.isHtmlLabel(state.cell) && mxClient.NO_FO &&
        graph.dialect == mxConstants.DIALECT_SVG;

    if (isForceHtml)
    {
        control.dialect = mxConstants.DIALECT_PREFERHTML;
        control.init(graph.container);
        control.node.style.zIndex = 1;
    }
    else
    {
        control.init(state.view.getOverlayPane());
    }

    var node = control.innerNode || control.node;

    // Workaround for missing click event on iOS is to check tolerance below
    if (clickHandler != null && !mxClient.IS_IOS)
    {
        if (graph.isEnabled())
        {
            node.style.cursor = 'pointer';
        }

        mxEvent.addListener(node, 'click', clickHandler);
    }

    if (handleEvents)
    {
        var first = null;

        mxEvent.addGestureListeners(node,
            function (evt)
            {
                first = new mxPoint(mxEvent.getClientX(evt), mxEvent.getClientY(evt));
                graph.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt, state));
                mxEvent.consume(evt);
            },
            function (evt)
            {
                graph.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt, state));
            },
            function (evt)
            {
                graph.fireMouseEvent(mxEvent.MOUSE_UP, new mxMouseEvent(evt, state));
                mxEvent.consume(evt);
            });

        // Uses capture phase for event interception to stop bubble phase
        if (clickHandler != null && mxClient.IS_IOS)
        {
            node.addEventListener('touchend', function(evt)
            {
                if (first != null)
                {
                    var tol = graph.tolerance;

                    if (Math.abs(first.x - mxEvent.getClientX(evt)) < tol &&
                        Math.abs(first.y - mxEvent.getClientY(evt)) < tol)
                    {
                        clickHandler.call(clickHandler, evt);
                        mxEvent.consume(evt);
                    }
                }
            }, true);
        }
    }

    return node;
};

/**
 * Function: isShapeEvent
 *
 * Returns true if the event is for the shape of the given state. This
 * implementation always returns true.
 *
 * Parameters:
 *
 * state - <mxCellState> whose shape fired the event.
 * evt - Mouse event which was fired.
 */
mxCellRenderer.prototype.isShapeEvent = function(state, evt)
{
    return true;
};

/**
 * Function: isLabelEvent
 *
 * Returns true if the event is for the label of the given state. This
 * implementation always returns true.
 *
 * Parameters:
 *
 * state - <mxCellState> whose label fired the event.
 * evt - Mouse event which was fired.
 */
mxCellRenderer.prototype.isLabelEvent = function(state, evt)
{
    return true;
};

/**
 * Function: installListeners
 *
 * Installs the event listeners for the given cell state.
 *
 * Parameters:
 *
 * state - <mxCellState> for which the event listeners should be isntalled.
 */
mxCellRenderer.prototype.installListeners = function(state)
{
    var graph = state.view.graph;

    // Workaround for touch devices routing all events for a mouse
    // gesture (down, move, up) via the initial DOM node. Same for
    // HTML images in all IE versions (VML images are working).
    var getState = function(evt)
    {
        var result = state;

        if ((graph.dialect != mxConstants.DIALECT_SVG && mxEvent.getSource(evt).nodeName == 'IMG') || mxClient.IS_TOUCH)
        {
            var x = mxEvent.getClientX(evt);
            var y = mxEvent.getClientY(evt);

            // Dispatches the drop event to the graph which
            // consumes and executes the source function
            var pt = mxUtils.convertPoint(graph.container, x, y);
            result = graph.view.getState(graph.getCellAt(pt.x, pt.y));
        }

        return result;
    };

    mxEvent.addGestureListeners(state.shape.node,
        mxUtils.bind(this, function(evt)
        {
            if (this.isShapeEvent(state, evt))
            {
                graph.fireMouseEvent(mxEvent.MOUSE_DOWN, new mxMouseEvent(evt, state));
            }
        }),
        mxUtils.bind(this, function(evt)
        {
            if (this.isShapeEvent(state, evt))
            {
                graph.fireMouseEvent(mxEvent.MOUSE_MOVE, new mxMouseEvent(evt, getState(evt)));
            }
        }),
        mxUtils.bind(this, function(evt)
        {
            if (this.isShapeEvent(state, evt))
            {
                graph.fireMouseEvent(mxEvent.MOUSE_UP, new mxMouseEvent(evt, getState(evt)));
            }
        })
    );

    // Uses double click timeout in mxGraph for quirks mode
    if (graph.nativeDblClickEnabled)
    {
        mxEvent.addListener(state.shape.node, 'dblclick',
            mxUtils.bind(this, function(evt)
            {
                if (this.isShapeEvent(state, evt))
                {
                    graph.dblClick(evt, state.cell);
                    mxEvent.consume(evt);
                }
            })
        );
    }
};

/**
 * Function: redrawLabel
 *
 * Redraws the label for the given cell state.
 *
 * Parameters:
 *
 * state - <mxCellState> whose label should be redrawn.
 */
mxCellRenderer.prototype.redrawLabel = function(state, forced)
{
    var graph = state.view.graph;
    var value = this.getLabelValue(state);
    var wrapping = graph.isWrapping(state.cell);
    var clipping = graph.isLabelClipped(state.cell);
    var isForceHtml = (state.view.graph.isHtmlLabel(state.cell) || (value != null && mxUtils.isNode(value)));
    var dialect = (isForceHtml) ? mxConstants.DIALECT_STRICTHTML : state.view.graph.dialect;
    var overflow = state.style[mxConstants.STYLE_OVERFLOW] || 'visible';

    if (state.text != null && (state.text.wrap != wrapping || state.text.clipped != clipping ||
        state.text.overflow != overflow || state.text.dialect != dialect))
    {
        state.text.destroy();
        state.text = null;
    }

    if (state.text == null && value != null && (mxUtils.isNode(value) || value.length > 0))
    {
        this.createLabel(state, value);
    }
    else if (state.text != null && (value == null || value.length == 0))
    {
        state.text.destroy();
        state.text = null;
    }

    if (state.text != null)
    {
        // Forced is true if the style has changed, so to get the updated
        // result in getLabelBounds we apply the new style to the shape
        if (forced)
        {
            // Checks if a full repaint is needed
            if (state.text.lastValue != null && this.isTextShapeInvalid(state, state.text))
            {
                // Forces a full repaint
                state.text.lastValue = null;
            }

            state.text.resetStyles();
            state.text.apply(state);

            // Special case where value is obtained via hook in graph
            state.text.valign = graph.getVerticalAlign(state);
        }

        var bounds = this.getLabelBounds(state);
        var nextScale = this.getTextScale(state);

        if (forced || state.text.value != value || state.text.isWrapping != wrapping ||
            state.text.overflow != overflow || state.text.isClipping != clipping ||
            state.text.scale != nextScale || state.text.dialect != dialect ||
            !state.text.bounds.equals(bounds))
        {
            // Forces an update of the text bounding box
            if (state.text.bounds.width != 0 && state.unscaledWidth != null &&
                Math.round((state.text.bounds.width /
                    state.text.scale * nextScale) - bounds.width) != 0)
            {
                state.unscaledWidth = null;
            }

            state.text.dialect = dialect;
            state.text.value = value;
            state.text.bounds = bounds;
            state.text.scale = nextScale;
            state.text.wrap = wrapping;
            state.text.clipped = clipping;
            state.text.overflow = overflow;

            // Preserves visible state
            var vis = state.text.node.style.visibility;
            this.redrawLabelShape(state.text);
            state.text.node.style.visibility = vis;
        }
    }
};

/**
 * Function: isTextShapeInvalid
 *
 * Returns true if the style for the text shape has changed.
 *
 * Parameters:
 *
 * state - <mxCellState> whose label should be checked.
 * shape - <mxText> shape to be checked.
 */
mxCellRenderer.prototype.isTextShapeInvalid = function(state, shape)
{
    function check(property, stylename, defaultValue)
    {
        var result = false;

        // Workaround for spacing added to directional spacing
        if (stylename == 'spacingTop' || stylename == 'spacingRight' ||
            stylename == 'spacingBottom' || stylename == 'spacingLeft')
        {
            result = parseFloat(shape[property]) - parseFloat(shape.spacing) !=
                (state.style[stylename] || defaultValue);
        }
        else
        {
            result = shape[property] != (state.style[stylename] || defaultValue);
        }

        return result;
    };

    return check('fontStyle', mxConstants.STYLE_FONTSTYLE, mxConstants.DEFAULT_FONTSTYLE) ||
        check('family', mxConstants.STYLE_FONTFAMILY, mxConstants.DEFAULT_FONTFAMILY) ||
        check('size', mxConstants.STYLE_FONTSIZE, mxConstants.DEFAULT_FONTSIZE) ||
        check('color', mxConstants.STYLE_FONTCOLOR, 'black') ||
        check('align', mxConstants.STYLE_ALIGN, '') ||
        check('valign', mxConstants.STYLE_VERTICAL_ALIGN, '') ||
        check('spacing', mxConstants.STYLE_SPACING, 2) ||
        check('spacingTop', mxConstants.STYLE_SPACING_TOP, 0) ||
        check('spacingRight', mxConstants.STYLE_SPACING_RIGHT, 0) ||
        check('spacingBottom', mxConstants.STYLE_SPACING_BOTTOM, 0) ||
        check('spacingLeft', mxConstants.STYLE_SPACING_LEFT, 0) ||
        check('horizontal', mxConstants.STYLE_HORIZONTAL, true) ||
        check('background', mxConstants.STYLE_LABEL_BACKGROUNDCOLOR) ||
        check('border', mxConstants.STYLE_LABEL_BORDERCOLOR) ||
        check('opacity', mxConstants.STYLE_TEXT_OPACITY, 100) ||
        check('textDirection', mxConstants.STYLE_TEXT_DIRECTION, mxConstants.DEFAULT_TEXT_DIRECTION);
};

/**
 * Function: redrawLabelShape
 *
 * Called to invoked redraw on the given text shape.
 *
 * Parameters:
 *
 * shape - <mxText> shape to be redrawn.
 */
mxCellRenderer.prototype.redrawLabelShape = function(shape)
{
    shape.redraw();
};

/**
 * Function: getTextScale
 *
 * Returns the scaling used for the label of the given state
 *
 * Parameters:
 *
 * state - <mxCellState> whose label scale should be returned.
 */
mxCellRenderer.prototype.getTextScale = function(state)
{
    return state.view.scale;
};

/**
 * Function: getLabelBounds
 *
 * Returns the bounds to be used to draw the label of the given state.
 *
 * Parameters:
 *
 * state - <mxCellState> whose label bounds should be returned.
 */
mxCellRenderer.prototype.getLabelBounds = function(state)
{
    var graph = state.view.graph;
    var scale = state.view.scale;
    var isEdge = graph.getModel().isEdge(state.cell);
    var bounds = new mxRectangle(state.absoluteOffset.x, state.absoluteOffset.y);

    if (isEdge)
    {
        var spacing = state.text.getSpacing();
        bounds.x += spacing.x * scale;
        bounds.y += spacing.y * scale;

        var geo = graph.getCellGeometry(state.cell);

        if (geo != null)
        {
            bounds.width = Math.max(0, geo.width * scale);
            bounds.height = Math.max(0, geo.height * scale);
        }
    }
    else
    {
        // Inverts label position
        if (state.text.isPaintBoundsInverted())
        {
            var tmp = bounds.x;
            bounds.x = bounds.y;
            bounds.y = tmp;
        }

        bounds.x += state.x;
        bounds.y += state.y;

        // Minimum of 1 fixes alignment bug in HTML labels
        bounds.width = Math.max(1, state.width);
        bounds.height = Math.max(1, state.height);

        var sc = mxUtils.getValue(state.style, mxConstants.STYLE_STROKECOLOR, mxConstants.NONE);

        if (sc != mxConstants.NONE && sc != '')
        {
            var s = parseFloat(mxUtils.getValue(state.style, mxConstants.STYLE_STROKEWIDTH, 1)) * scale;
            var dx = 1 + Math.floor((s - 1) / 2);
            var dh = Math.floor(s + 1);

            bounds.x += dx;
            bounds.y += dx;
            bounds.width -= dh;
            bounds.height -= dh;
        }
    }

    if (state.text.isPaintBoundsInverted())
    {
        // Rotates around center of state
        var t = (state.width - state.height) / 2;
        bounds.x += t;
        bounds.y -= t;
        var tmp = bounds.width;
        bounds.width = bounds.height;
        bounds.height = tmp;
    }

    // Shape can modify its label bounds
    if (state.shape != null)
    {
        var hpos = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_POSITION, mxConstants.ALIGN_CENTER);
        var vpos = mxUtils.getValue(state.style, mxConstants.STYLE_VERTICAL_LABEL_POSITION, mxConstants.ALIGN_MIDDLE);

        if (hpos == mxConstants.ALIGN_CENTER && vpos == mxConstants.ALIGN_MIDDLE)
        {
            bounds = state.shape.getLabelBounds(bounds);
        }
    }

    // Label width style overrides actual label width
    var lw = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_WIDTH, null);

    if (lw != null)
    {
        bounds.width = parseFloat(lw) * scale;
    }

    if (!isEdge)
    {
        this.rotateLabelBounds(state, bounds);
    }

    return bounds;
};

/**
 * Function: rotateLabelBounds
 *
 * Adds the shape rotation to the given label bounds and
 * applies the alignment and offsets.
 *
 * Parameters:
 *
 * state - <mxCellState> whose label bounds should be rotated.
 * bounds - <mxRectangle> the rectangle to be rotated.
 */
mxCellRenderer.prototype.rotateLabelBounds = function(state, bounds)
{
    bounds.y -= state.text.margin.y * bounds.height;
    bounds.x -= state.text.margin.x * bounds.width;

    if (!this.legacySpacing || (state.style[mxConstants.STYLE_OVERFLOW] != 'fill' && state.style[mxConstants.STYLE_OVERFLOW] != 'width'))
    {
        var s = state.view.scale;
        var spacing = state.text.getSpacing();
        bounds.x += spacing.x * s;
        bounds.y += spacing.y * s;

        var hpos = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_POSITION, mxConstants.ALIGN_CENTER);
        var vpos = mxUtils.getValue(state.style, mxConstants.STYLE_VERTICAL_LABEL_POSITION, mxConstants.ALIGN_MIDDLE);
        var lw = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_WIDTH, null);

        bounds.width = Math.max(0, bounds.width - ((hpos == mxConstants.ALIGN_CENTER && lw == null) ? (state.text.spacingLeft * s + state.text.spacingRight * s) : 0));
        bounds.height = Math.max(0, bounds.height - ((vpos == mxConstants.ALIGN_MIDDLE) ? (state.text.spacingTop * s + state.text.spacingBottom * s) : 0));
    }

    var theta = state.text.getTextRotation();

    // Only needed if rotated around another center
    if (theta != 0 && state != null && state.view.graph.model.isVertex(state.cell))
    {
        var cx = state.getCenterX();
        var cy = state.getCenterY();

        if (bounds.x != cx || bounds.y != cy)
        {
            var rad = theta * (Math.PI / 180);
            var pt = mxUtils.getRotatedPoint(new mxPoint(bounds.x, bounds.y),
                Math.cos(rad), Math.sin(rad), new mxPoint(cx, cy));

            bounds.x = pt.x;
            bounds.y = pt.y;
        }
    }
};

/**
 * Function: redrawCellOverlays
 *
 * Redraws the overlays for the given cell state.
 *
 * Parameters:
 *
 * state - <mxCellState> whose overlays should be redrawn.
 */
mxCellRenderer.prototype.redrawCellOverlays = function(state, forced)
{
    this.createCellOverlays(state);

    if (state.overlays != null)
    {
        var rot = mxUtils.mod(mxUtils.getValue(state.style, mxConstants.STYLE_ROTATION, 0), 90);
        var rad = mxUtils.toRadians(rot);
        var cos = Math.cos(rad);
        var sin = Math.sin(rad);

        state.overlays.visit(function(id, shape)
        {
            var bounds = shape.overlay.getBounds(state);

            if (!state.view.graph.getModel().isEdge(state.cell))
            {
                if (state.shape != null && rot != 0)
                {
                    var cx = bounds.getCenterX();
                    var cy = bounds.getCenterY();

                    var point = mxUtils.getRotatedPoint(new mxPoint(cx, cy), cos, sin,
                        new mxPoint(state.getCenterX(), state.getCenterY()));

                    cx = point.x;
                    cy = point.y;
                    bounds.x = Math.round(cx - bounds.width / 2);
                    bounds.y = Math.round(cy - bounds.height / 2);
                }
            }

            if (forced || shape.bounds == null || shape.scale != state.view.scale ||
                !shape.bounds.equals(bounds))
            {
                shape.bounds = bounds;
                shape.scale = state.view.scale;
                shape.redraw();
            }
        });
    }
};

/**
 * Function: redrawControl
 *
 * Redraws the control for the given cell state.
 *
 * Parameters:
 *
 * state - <mxCellState> whose control should be redrawn.
 */
mxCellRenderer.prototype.redrawControl = function(state, forced)
{
    var image = state.view.graph.getFoldingImage(state);

    if (state.control != null && image != null)
    {
        var bounds = this.getControlBounds(state, image.width, image.height);
        var r = (this.legacyControlPosition) ?
            mxUtils.getValue(state.style, mxConstants.STYLE_ROTATION, 0) :
            state.shape.getTextRotation();
        var s = state.view.scale;

        if (forced || state.control.scale != s || !state.control.bounds.equals(bounds) ||
            state.control.rotation != r)
        {
            state.control.rotation = r;
            state.control.bounds = bounds;
            state.control.scale = s;

            state.control.redraw();
        }
    }
};

/**
 * Function: getControlBounds
 *
 * Returns the bounds to be used to draw the control (folding icon) of the
 * given state.
 */
mxCellRenderer.prototype.getControlBounds = function(state, w, h)
{
    if (state.control != null)
    {
        var s = state.view.scale;
        var cx = state.getCenterX();
        var cy = state.getCenterY();

        if (!state.view.graph.getModel().isEdge(state.cell))
        {
            cx = state.x + w * s;
            cy = state.y + h * s;

            if (state.shape != null)
            {
                // TODO: Factor out common code
                var rot = state.shape.getShapeRotation();

                if (this.legacyControlPosition)
                {
                    rot = mxUtils.getValue(state.style, mxConstants.STYLE_ROTATION, 0);
                }
                else
                {
                    if (state.shape.isPaintBoundsInverted())
                    {
                        var t = (state.width - state.height) / 2;
                        cx += t;
                        cy -= t;
                    }
                }

                if (rot != 0)
                {
                    var rad = mxUtils.toRadians(rot);
                    var cos = Math.cos(rad);
                    var sin = Math.sin(rad);

                    var point = mxUtils.getRotatedPoint(new mxPoint(cx, cy), cos, sin,
                        new mxPoint(state.getCenterX(), state.getCenterY()));
                    cx = point.x;
                    cy = point.y;
                }
            }
        }

        return (state.view.graph.getModel().isEdge(state.cell)) ?
            new mxRectangle(Math.round(cx - w / 2 * s), Math.round(cy - h / 2 * s), Math.round(w * s), Math.round(h * s))
            : new mxRectangle(Math.round(cx - w / 2 * s), Math.round(cy - h / 2 * s), Math.round(w * s), Math.round(h * s));
    }

    return null;
};

/**
 * Function: insertStateAfter
 *
 * Inserts the given array of <mxShapes> after the given nodes in the DOM.
 *
 * Parameters:
 *
 * shapes - Array of <mxShapes> to be inserted.
 * node - Node in <drawPane> after which the shapes should be inserted.
 * htmlNode - Node in the graph container after which the shapes should be inserted that
 * will not go into the <drawPane> (eg. HTML labels without foreignObjects).
 */
mxCellRenderer.prototype.insertStateAfter = function(state, node, htmlNode)
{
    var shapes = this.getShapesForState(state);

    for (var i = 0; i < shapes.length; i++)
    {
        if (shapes[i] != null && shapes[i].node != null)
        {
            var html = shapes[i].node.parentNode != state.view.getDrawPane() &&
                shapes[i].node.parentNode != state.view.getOverlayPane();
            var temp = (html) ? htmlNode : node;

            if (temp != null && temp.nextSibling != shapes[i].node)
            {
                if (temp.nextSibling == null)
                {
                    temp.parentNode.appendChild(shapes[i].node);
                }
                else
                {
                    temp.parentNode.insertBefore(shapes[i].node, temp.nextSibling);
                }
            }
            else if (temp == null)
            {
                // Special case: First HTML node should be first sibling after canvas
                if (shapes[i].node.parentNode == state.view.graph.container)
                {
                    var canvas = state.view.canvas;

                    while (canvas != null && canvas.parentNode != state.view.graph.container)
                    {
                        canvas = canvas.parentNode;
                    }

                    if (canvas != null && canvas.nextSibling != null)
                    {
                        if (canvas.nextSibling != shapes[i].node)
                        {
                            shapes[i].node.parentNode.insertBefore(shapes[i].node, canvas.nextSibling);
                        }
                    }
                    else
                    {
                        shapes[i].node.parentNode.appendChild(shapes[i].node);
                    }
                }
                else if (shapes[i].node.parentNode.firstChild != null && shapes[i].node.parentNode.firstChild != shapes[i].node)
                {
                    // Inserts the node as the first child of the parent to implement the order
                    shapes[i].node.parentNode.insertBefore(shapes[i].node, shapes[i].node.parentNode.firstChild);
                }
            }

            if (html)
            {
                htmlNode = shapes[i].node;
            }
            else
            {
                node = shapes[i].node;
            }
        }
    }

    return [node, htmlNode];
};

/**
 * Function: getShapesForState
 *
 * Returns the <mxShapes> for the given cell state in the order in which they should
 * appear in the DOM.
 *
 * Parameters:
 *
 * state - <mxCellState> whose shapes should be returned.
 */
mxCellRenderer.prototype.getShapesForState = function(state)
{
    return [state.shape, state.text, state.control];
};

/**
 * Function: redraw
 *
 * Updates the bounds or points and scale of the shapes for the given cell
 * state. This is called in mxGraphView.validatePoints as the last step of
 * updating all cells.
 *
 * Parameters:
 *
 * state - <mxCellState> for which the shapes should be updated.
 * force - Optional boolean that specifies if the cell should be reconfiured
 * and redrawn without any additional checks.
 * rendering - Optional boolean that specifies if the cell should actually
 * be drawn into the DOM. If this is false then redraw and/or reconfigure
 * will not be called on the shape.
 */
mxCellRenderer.prototype.redraw = function(state, force, rendering)
{
    var shapeChanged = this.redrawShape(state, force, rendering);

    if (state.shape != null && (rendering == null || rendering))
    {
        this.redrawLabel(state, shapeChanged);
        this.redrawCellOverlays(state, shapeChanged);
        this.redrawControl(state, shapeChanged);
    }
};

/**
 * Function: redrawShape
 *
 * Redraws the shape for the given cell state.
 *
 * Parameters:
 *
 * state - <mxCellState> whose label should be redrawn.
 */
mxCellRenderer.prototype.redrawShape = function(state, force, rendering)
{
    var model = state.view.graph.model;
    var shapeChanged = false;

    // Forces creation of new shape if shape style has changed
    if (state.shape != null && state.shape.style != null && state.style != null &&
        state.shape.style[mxConstants.STYLE_SHAPE] != state.style[mxConstants.STYLE_SHAPE])
    {
        state.shape.destroy();
        state.shape = null;
    }

    if (state.shape == null && state.view.graph.container != null &&
        state.cell != state.view.currentRoot &&
        (model.isVertex(state.cell) || model.isEdge(state.cell)))
    {
        state.shape = this.createShape(state);

        if (state.shape != null)
        {
            state.shape.minSvgStrokeWidth = this.minSvgStrokeWidth;
            state.shape.antiAlias = this.antiAlias;

            this.createIndicatorShape(state);
            this.initializeShape(state);
            this.createCellOverlays(state);
            this.installListeners(state);

            // Forces a refresh of the handler if one exists
            state.view.graph.selectionCellsHandler.updateHandler(state);
        }
    }
    else if (!force && state.shape != null && (!mxUtils.equalEntries(state.shape.style,
        state.style) || this.checkPlaceholderStyles(state)))
    {
        state.shape.resetStyles();
        this.configureShape(state);
        // LATER: Ignore update for realtime to fix reset of current gesture
        state.view.graph.selectionCellsHandler.updateHandler(state);
        force = true;
    }

    if (state.shape != null)
    {
        // Handles changes of the collapse icon
        this.createControl(state);

        // Redraws the cell if required, ignores changes to bounds if points are
        // defined as the bounds are updated for the given points inside the shape
        if (force || this.isShapeInvalid(state, state.shape))
        {
            if (state.absolutePoints != null)
            {
                state.shape.points = state.absolutePoints.slice();
                state.shape.bounds = null;
            }
            else
            {
                state.shape.points = null;
                state.shape.bounds = new mxRectangle(state.x, state.y, state.width, state.height);
            }

            state.shape.scale = state.view.scale;

            if (rendering == null || rendering)
            {
                this.doRedrawShape(state);
            }
            else
            {
                state.shape.updateBoundingBox();
            }

            shapeChanged = true;
        }
    }

    return shapeChanged;
};

/**
 * Function: doRedrawShape
 *
 * Invokes redraw on the shape of the given state.
 */
mxCellRenderer.prototype.doRedrawShape = function(state)
{
    state.shape.redraw();
};

/**
 * Function: isShapeInvalid
 *
 * Returns true if the given shape must be repainted.
 */
mxCellRenderer.prototype.isShapeInvalid = function(state, shape)
{
    return shape.bounds == null || shape.scale != state.view.scale ||
        (state.absolutePoints == null && !shape.bounds.equals(state)) ||
        (state.absolutePoints != null && !mxUtils.equalPoints(shape.points, state.absolutePoints))
};

/**
 * Function: destroy
 *
 * Destroys the shapes associated with the given cell state.
 *
 * Parameters:
 *
 * state - <mxCellState> for which the shapes should be destroyed.
 */
mxCellRenderer.prototype.destroy = function(state)
{
    if (state.shape != null)
    {
        if (state.text != null)
        {
            state.text.destroy();
            state.text = null;
        }

        if (state.overlays != null)
        {
            state.overlays.visit(function(id, shape)
            {
                shape.destroy();
            });

            state.overlays = null;
        }

        if (state.control != null)
        {
            state.control.destroy();
            state.control = null;
        }

        state.shape.destroy();
        state.shape = null;
    }
};
